/* eslint-disable no-underscore-dangle */
// https://wiki.maoyan.com/pages/viewpage.action?pageId=154844268

// https://wiki.maoyan.com/pages/viewpage.action?pageId=154852894
(function () {
    function report(payload = {}) {
      const { pathName } = payload;
      const urlObject = new URL(pathName);
      const { pathname, hostname } = urlObject;
      // 转为为不带http 协议和qs的路径
      const path = hostname + pathname;
      fetch('/api/view/response/log/add', {
        body: JSON.stringify({
          ...payload,
          pathName: path,
          pageUrl: pathName,
        }),
        cache: 'no-cache',
        credentials: 'omit', // include, same-origin, *omit
        headers: {
          'content-type': 'application/json',
        },
        method: 'POST',
        mode: 'cors', // no-cors, cors, *same-origin
        redirect: 'follow', // manual, *follow, error
        referrer: 'no-referrer', // *client, no-referrer
      })
        .then(() => {
          // owlDebug(response, 'response');
        })
        .catch(() => {
          /** 将上报失败的网络错误存放到本地，下次再上报 */
          const { reportType } = payload;
          if (reportType === 'error') {
            const { data } = payload;
            const failedNetWorkErrors = data.filter((_data_) => {
              return !!_data_.isNet;
            });
            // window._networkErr_.storeToFaileds(failedNetWorkErrors);
          }
        });
    }
  
    /** 自定义debug方法，不挟持console.debug 以免其他人正常使用console.debug会出问题 */
    const owl_debug_key = 'owl_debug';
    const debugFlag = window.localStorage.getItem(owl_debug_key);
    const owlDebug = (...args) => {
      if (!!debugFlag) {
        console.debug(...args);
      }
    };
  
    if (window.hkOwl) {
      console.error('window.hkOwl already exists!');
    }
  
    /** 用户属性相关 */
    let __hkOwlUser__ = null;
    const hkOwlUser = {
      set(user) {
        __hkOwlUser__ = user;
      },
      get() {
        return __hkOwlUser__;
      },
    };
    window.__hkOwlUser__ = hkOwlUser;
    /** 用户属性相关 */
  
    /** 网络错误相关 */
    const network_err_key = 'network_err_key';
    const networkErrModel = {
      time: '', // 上报时间
      errors: [], // 今日(当前)发生的错误
      pastErrors: [], // 过去的错误
      faileds: [], // 上报失败的错误
    };
    const maxErrorsLength = 100;
    const maxFailedsLength = 100;
    const networkErr = {
      /** 获取错误上报的信息 */
      get() {
        return window.localStorage.getItem(network_err_key);
      },
      /** 获取可以上报的信息，又errors + faileds + pasht组成 */
      getNetWorkErrToReport() {
        let errorsToReport;
        try {
          const networkErrInfo = this.get();
          if (networkErrInfo) {
            const errorInfoObject = JSON.parse(networkErrInfo);
            const { errors, pastErrors, faileds } = errorInfoObject;
            // 当前，过期，失败错误全上报
            errorsToReport = [...errors, ...pastErrors, ...faileds];
          } else {
            errorsToReport = [];
          }
        } catch (error) {
          errorsToReport = [];
          owlDebug(error, '获取上报的网络错误失败');
        }
        this.clear();
        return errorsToReport;
      },
      /** 过滤错误上报信息 */
      filterErrData(data) {
        // production 环境才进行过滤
        if (MYENV !== 'production') return data;
        const err = JSON.parse(JSON.stringify(data));
        if (err?.error && err.error instanceof Object) {
          // params、requesData 可能包含敏感信息.所以不进行上报
          if (err.error.params) {
            delete err.error.params;
          }
          if (err.error.requesData) {
            delete err.error.requesData;
          }
        }
        return err;
      },
      /** 将错误上报的信息存放到本地 */
      set(data) {
        const err = this.filterErrData(data);
        owlDebug(err, 'set net work err');
        const networkErrInfo = this.get();
        if (err?.error && err.error instanceof Object) {
          err.error.PLUS_JOB_ID = PLUS_JOB_ID;
        }
        // 为空，则尚未存放过错误
        if (!networkErrInfo) {
          const errSetedTime = new Date().getTime();
          const errors = [err];
          const faileds = [];
          const newNetworkErrInfoObject = {
            time: errSetedTime,
            errors: errors,
            pastErrors: [],
            faileds: faileds,
          };
          const newNetworkErrInfoString = JSON.stringify(newNetworkErrInfoObject);
          window.localStorage.setItem(network_err_key, newNetworkErrInfoString);
        } else {
          // 已经存放过，则将其反序列化，然后取出其errors对象，只存放100个
          try {
            const newNetworkErrInfoObject = JSON.parse(networkErrInfo);
            const { errors, time } = newNetworkErrInfoObject;
            const pastDate = new Date(time);
            const now = new Date();
            const pastSetTime = `${pastDate.getFullYear()}-${pastDate.getMonth()}-${pastDate.getDate()}`;
            const currentTime = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
            /** 如果上次设置的时间与当前设置的时间不一样，则将上次未上报的，存放到pastErrors, 并清空errors */
            if (pastSetTime !== currentTime) {
              newNetworkErrInfoObject.pastErrors = [
                ...newNetworkErrInfoObject.pastErrors,
                ...errors,
              ];
              newNetworkErrInfoObject.errors = [];
            }
  
            newNetworkErrInfoObject.errors.push(err);
            // 取最新100个
            const newErrors = newNetworkErrInfoObject.errors.slice(
              0 - maxErrorsLength,
            );
            newNetworkErrInfoObject.time = now.getTime();
            newNetworkErrInfoObject.errors = newErrors;
            const newNetworkErrInfoString = JSON.stringify(
              newNetworkErrInfoObject,
            );
            window.localStorage.setItem(network_err_key, newNetworkErrInfoString);
          } catch (e) {
            owlDebug(e, '存放网络错误失败');
            /** 存放失败错误处理, 清空，重新存放 */
            window.localStorage.removeItem(network_err_key);
            this.set(err);
          }
        }
      },
      /** 上报后，清空当前存放的信息，errors, pastErrors 和 faileds */
      clear() {
        try {
          const networkErrInfo = this.get();
          const errorInfoObject = JSON.parse(networkErrInfo);
          errorInfoObject.errors = [];
          errorInfoObject.pastErrors = [];
          errorInfoObject.faileds = [];
          const newNetworkErrInfoString = JSON.stringify(errorInfoObject);
          window.localStorage.setItem(network_err_key, newNetworkErrInfoString);
        } catch (error) {
          window.localStorage.removeItem(network_err_key);
        }
      },
      /** 上报失败时，暂时存放到本地，等待下次上报 */
      storeToFaileds(err) {
        const networkErrInfo = this.get();
        let errorInfoObject = JSON.parse(networkErrInfo);
        // 防止为null时候报错
        if (!errorInfoObject) {
          const errSetedTime = new Date().getTime();
          const errors = [];
          const faileds = [];
          errorInfoObject = {
            time: errSetedTime,
            errors: errors,
            pastErrors: [],
            faileds: faileds,
          };
        }
        errorInfoObject.faileds.push(...err);
        /** 只存放100个 */
        const newFailedErrors = errorInfoObject.faileds.slice(
          0 - maxFailedsLength,
        );
        errorInfoObject.faileds = newFailedErrors;
        const newNetworkErrInfoString = JSON.stringify(errorInfoObject);
        window.localStorage.setItem(network_err_key, newNetworkErrInfoString);
      },
    };
    window._networkErr_ = networkErr;
    /** 网络错误相关 */
  
    const errorStacks = [];
    const errorReportTimerTick = 10000;
    const mutationTimerTick = 3000;
    let errorReportTimer = null;
    const resourceMonitorTimerTick = 15000; // 30000;
    /** 进入页面后的操作时间, 用户操作onclik时记录，切换页面后重置为null */
    let operationTimeInLifecycle = null;
    /** 进入页面后的操作次数，用户onclick时记录新增，切换页面后重置为0 */
    let operationCounts = 0;
    /** 用于 标记fst 是否已经上报，上报过，则在离开页面是不上报，否则上报一次(应对在3s内离开页面没有记录到fst的情况)， 切换页面后重置为false */
    let hasReportFirstScreen = false;
    let isReload = true;
    let currentUrl = '';
    let resourceMonitorIndex = 0;
    let firstScreenResourceIndex = 0;
    let initialTime = 0;
    let outBoundingCount = 0;
    const mutationRecords = [];
    let observer = null;
    let mutationTimer = null;
    let resourceMonitorTimer = null;
    // 选择需要观察变动的节点
    const targetNode = document.body;
    // 观察器的配置（需要观察什么变动）
    const config = { attributes: false, childList: true, subtree: true };
    const mutationTimerCallback = function () {
      // 之后，可停止观察
      if (observer) {
        observer.disconnect();
      }
      // 停止监听后，开始对记录到的 mutationRecords 做处理。遍历 mutationRecords，筛选每一条记录的 nodes 中的首屏内可见节点，
      let firstScreenMutationEndTime = 0;
      owlDebug('timer...');
      if (mutationRecords.length) {
        owlDebug(mutationRecords, 'mutationRecords......');
        const record = mutationRecords[mutationRecords.length - 1];
        firstScreenMutationEndTime = record.startTime - initialTime;
      }
      const resources = window.performance.getEntriesByType('resource');
      const currentResourceLength = resources.length;
      let lastImgLoadTime = 0;
      for (let i = firstScreenResourceIndex; i < currentResourceLength; i++) {
        const resource = resources[i];
        if (
          resource.initiatorType === 'img' &&
          lastImgLoadTime < resource.responseEnd
        ) {
          lastImgLoadTime = resource.responseEnd;
        }
      }
      if (lastImgLoadTime) {
        lastImgLoadTime -= initialTime;
      }
      const operationTime = operationTimeInLifecycle
        ? operationTimeInLifecycle - initialTime
        : null;
      owlDebug(
        'firstScreenMutationEndTime, lastImgLoadTime, operationTime = ',
        firstScreenMutationEndTime,
        lastImgLoadTime,
        operationTime,
      );
  
      try {
        const firstScreenTime = Math.max(
          lastImgLoadTime,
          firstScreenMutationEndTime,
        );
  
        owlDebug('currentUrl =', currentUrl);
        owlDebug('firstScreenTime = ', firstScreenTime);
        // @todo 上报首屏时间 firstScreenTime，XMLHttpRequest 自行实现
        report({
          pathName: currentUrl,
          reportType: 'firstScreenTime',
          data: [
            {
              mutationTime: firstScreenMutationEndTime,
              imgLoadTime: lastImgLoadTime,
              operationTime,
              operationCounts,
              isReload,
            },
          ],
        });
        isReload = false;
        hasReportFirstScreen = true;
      } catch (e) {
        console.error('上报首屏时间异常：', e);
      }
    };
    const resourceMonitorTimerCallback = function () {
      const resources = window.performance.getEntriesByType('resource');
      const currentResourceLength = resources.length;
      const validResources = [];
      const validApis = [];
      for (let i = 0; i < currentResourceLength; i++) {
        const resource = resources[i];
        // const transferSize = resource.transferSize;
        const { duration } = resource;
        const { initiatorType } = resource;
        // 无传输内容，传输时间小于 10ms，认为是读本地缓存，不上报
        // (css 文件读本地缓存时 duration 值为 0 ，但 transferSize 不为 0；另外 img 非缓存获取时，duration 值正常，但获取到 transferSize 值永远为 0，很奇怪)
        if (initiatorType !== 'fetch' && duration < 10) {
          continue;
        }
        const { name } = resource;
        /**  不上报 打点请求的结果 */
        if (!name.includes('/view/response/log/add')) {
          // api 请求
          if (initiatorType === 'fetch') {
            const urlObject = new URL(name);
            const resourceObject = {};
            const keys = Object.keys(resource);
            for (let key in resource) {
              resourceObject[key] = resource[key];
            }
            delete resourceObject.toJSON;
            resourceObject.name = urlObject.pathname;
            resourceObject.originName = name;
            validApis.push(resourceObject);
          } else {
            // 其他资源
            validResources.push(resource);
          }
        }
      }
      /** 统计好资源后，清除现有资源,以免误报 */
      performance.clearResourceTimings();
      if (validApis.length) {
        try {
          owlDebug('validApis = ', validApis);
          // @todo 上报 api 请求日志，XMLHttpRequest 自行实现
          report({
            pathName: currentUrl,
            reportType: 'apiTime',
            data: validApis,
          });
        } catch (e) {
          console.error('上报 api 请求日志异常：', e);
        }
      }
      if (validResources.length) {
        try {
          owlDebug('validResources = ', validResources);
          // @todo 上报资源日志，XMLHttpRequest 自行实现
          report({
            pathName: currentUrl,
            reportType: 'staticResourceTime',
            data: validResources,
          });
        } catch (e) {
          console.error('上报资源日志异常：', e);
        }
      }
      setTimeout(arguments.callee, resourceMonitorTimerTick);
    };
    // 当观察到变动时执行的回调函数
    const callback = function (mutationsList, observer) {
      owlDebug('callback....', performance.now());
      this._startMutaionTimer();
      if (observer && operationCounts > 0) {
        observer.disconnect();
      }
      for (let i = 0; i < mutationsList.length; i++) {
        const mutation = mutationsList[i];
        owlDebug('mutation = ', mutation, performance.now());
        if (mutation.type === 'childList') {
          if (mutation.addedNodes.length) {
            let inBounding = true;
            for (let j = 0; j < mutation.addedNodes.length; j++) {
              const addNode = mutation.addedNodes[j];
              if (!addNode || !addNode.getBoundingClientRect) {
                continue;
              }
              const boundingClientRect = addNode.getBoundingClientRect();
              const outBounding =
                boundingClientRect.left >= window.innerWidth ||
                boundingClientRect.left + boundingClientRect.width <= 0 ||
                boundingClientRect.top >= window.innerHeight ||
                boundingClientRect.top + boundingClientRect.height <= 0;
              owlDebug(
                'addNode, boundingClientRect, outBounding = ',
                addNode,
                boundingClientRect,
                outBounding,
                performance.now(),
              );
              if (outBounding) {
                outBoundingCount++;
                if (outBoundingCount === 15) {
                  // 之后，可停止观察
                  if (observer) {
                    owlDebug('disconnected', performance.now());
                    observer.disconnect();
                  }
                }
                inBounding = false;
              }
            }
            // 只缓存在可视区域内的元素
            if (inBounding) {
              owlDebug('add to records...', performance.now());
              mutationRecords.push({
                nodes: mutation.addedNodes,
                startTime: window.performance.now(),
              });
            }
          }
        }
      }
    };
    /** 上报错误 */
    const reportErrorCallback = function () {
      const networkErrors = networkErr.getNetWorkErrToReport();
      const errorStoBeReported = [...networkErrors, ...errorStacks];
      /** 如果error 字段存在，则将error字段, 转为json字符串，以便记录在服务端 */
      const formattedErrors = errorStoBeReported.map((e) => {
        const formattedError = { ...e };
        if (formattedError.error) {
          try {
            formattedError.error.user = hkOwlUser.get();
            formattedError.message = JSON.stringify(formattedError.error);
          } catch (error) {
            owlDebug(error, '格式化error 错误');
          }
        }
        return formattedError;
      });
      try {
        if (formattedErrors.length > 0) {
          owlDebug('report error');
          report({
            pathName: currentUrl,
            reportType: 'error',
            data: [...formattedErrors],
          });
          errorStacks.length = 0;
        }
      } catch (error) {
        console.error(error);
      }
      setTimeout(arguments.callee, errorReportTimerTick);
    };
    /** 上报错误 */
    /** 监听操作记录, 使用捕获模式，优先于其他的点击事件触发  */
    document.body.addEventListener(
      'click',
      () => {
        /** 若然未记录操作时间, 则记录操作时间 */
        if (!operationTimeInLifecycle) {
          owlDebug('记录o-t');
          operationTimeInLifecycle = performance.now();
        }
        operationCounts += 1;
      },
      true,
    );
    /** 监听操作记录 */
    /** 监听错误 */
    window.addEventListener('error', (event) => {
      const { message, filename, colno, lineno, error } = event;
      const errObject = {
        location: window.location.href,
      };
      // 忽略 ResizeObserver loop 报出的错误
      const ignore = (message || '').includes('ResizeObserver loop');
      if (!ignore) {
        /** error 可能为null */
        try {
          errObject.eventMessage = message;
          errObject.message = error.message;
          errObject.stack = error.stack;
          errObject.errorFrom = 'script';
        } catch {
          errObject.message = null;
          errObject.stack = null;
          errObject.eventMessage = message;
          errObject.errorFrom = 'script';
        }
  
        const e = { message, filename, colno, lineno, error: errObject };
        errorStacks.push(e);
      }
      owlDebug(event, 'error event');
    });
    window.addEventListener('unhandledrejection', (event) => {
      const e = {};
      const { reason } = event;
      try {
        const { message } = reason || {};
        const errorObject = {};
        errorObject.message = reason.message;
        errorObject.stack = reason.stack;
        if (!errorObject.message && !errorObject.stack) {
          errorObject.message = JSON.stringify(reason);
          errorObject.reason = '非错误rejection，可能由组件或业务方主动抛出';
        }
        errorObject.errorFrom = 'promise';
        errorObject.location = window.location.href;
        e.message = message;
        e.error = errorObject;
      } catch {
        e.message = '记录unhandledrejection失败';
        const errorObject = {
          reason,
          errorFrom: 'promise',
          location: window.location.href,
        };
        e.error = errorObject;
      }
      errorStacks.push(e);
      owlDebug(event, 'promise error event');
    });
    // 监听图片和脚本加载错误
    document.addEventListener(
      'error',
      (e) => {
        let errorObject = {};
        // 图片
        if (e && e.target && e.target instanceof HTMLImageElement) {
          errorObject.error = {
            type: `${window.location.hostname}_resouceError`,
            src: e.target.src,
          };
          errorStacks.push(errorObject);
        }
        // script
        if (e && e.target && e.target instanceof HTMLScriptElement) {
          errorObject.error = {
            type: `${window.location.hostname}_resouceError`,
            src: e.target.src,
          };
          errorStacks.push(errorObject);
        }
      },
      true,
    );
    /** 监听错误 */
    window.hkOwl = {
      start(url) {
        owlDebug('start...', performance.now());
        this._setCurrentPath(url);
        this._bindMutationObserver();
        this._startNavigationMonitor();
        this._startResourceMonitor();
        this._startErrorMonitor();
      },
      reset(url) {
        owlDebug('reset...', performance.now());
        this._reportFSTBeforeReset();
        this._setCurrentPath(url);
        this._resetInitialData();
        this._startFirstScreenMonitor();
      },
      /** 切换场景，重置数据前调用，将未上报的fst上报，以免丢失 */
      _reportFSTBeforeReset() {
        if (!hasReportFirstScreen) {
          owlDebug('弥补式上报');
          mutationTimerCallback();
        }
      },
      _setCurrentPath(url) {
        const { location } = window;
        const { origin } = location;
        currentUrl = origin + url;
      },
      _bindMutationObserver() {
        // 创建一个观察器实例并传入回调函数
        observer = new MutationObserver(callback.bind(this));
        this._startFirstScreenMonitor();
      },
      _startFirstScreenMonitor() {
        // 以上述配置开始观察目标节点
        observer.observe(targetNode, config);
        // this._startMutaionTimer();
      },
      _startMutaionTimer() {
        window.addEventListener('unload', function () {
          if (mutationTimer) {
            clearTimeout(mutationTimer);
            mutationTimer = null;
          }
        });
        if (mutationTimer) {
          clearTimeout(mutationTimer);
          mutationTimer = null;
        }
        mutationTimer = setTimeout(
          mutationTimerCallback.bind(this),
          mutationTimerTick,
        );
      },
      _resetInitialData() {
        owlDebug('reset');
        outBoundingCount = 0;
        mutationRecords.length = 0;
        initialTime = performance.now();
        operationTimeInLifecycle = null;
        operationCounts = 0;
        hasReportFirstScreen = false;
        firstScreenResourceIndex = window.performance.getEntriesByType('resource')
          .length;
        if (observer) {
          observer.disconnect();
        }
      },
      _startResourceMonitor() {
        window.addEventListener('unload', function () {
          if (resourceMonitorTimer) {
            clearTimeout(resourceMonitorTimer);
            resourceMonitorTimer = null;
          }
        });
        resourceMonitorTimer = setTimeout(
          resourceMonitorTimerCallback,
          resourceMonitorTimerTick,
        );
      },
      _startNavigationMonitor() {
        window.addEventListener('load', function () {
          const navigations = window.performance.getEntriesByType('navigation');
          if (navigations.length) {
            try {
              owlDebug('navigation = ', navigations[0]);
              // @todo 上报 navigation 日志，XMLHttpRequest 自行实现
              report({
                pathName: currentUrl,
                reportType: 'navigationTime',
                data: [navigations[0]],
              });
            } catch (e) {
              console.error('上报导航日志异常：', e);
            }
          }
        });
      },
      _startErrorMonitor() {
        window.addEventListener('unload', function () {
          if (errorReportTimer) {
            clearTimeout(errorReportTimer);
            errorReportTimer = null;
          }
          reportErrorCallback();
        });
        errorReportTimer = setTimeout(reportErrorCallback, errorReportTimerTick);
      },
    };
  
    /** 获取初次进入的location 并设置场景 */
    const url = window.location.pathname;
    window.hkOwl.start(url);
    /** 通过 重写与react/history 中相同的api, 来监听history的变动，从而起到监听的效果 */
    const { pushState, replaceState } = history;
    history.pushState = (...args) => {
      try {
        const { pathname } = window.location;
        const url = args[2];
        owlDebug('current_pathname', pathname);
        owlDebug('url_push', url);
        /** 如果push进入页面，不为/seller/login 则重置状态, 否则认为此次push为未登录的重定向，不重置状态 */
        if (url !== '/seller/login') {
          /** reset数据时，先上报当前场景剩余未上报的资源 */
          resourceMonitorTimerCallback();
          window.hkOwl.reset(url);
        }
        // 切换场景, 在此处调用是因为，考虑到从 a -> /seller/login时，没有reset数据，但是已经重定向到/seller/login 所以，还是要切换场景
        window.hkOwl._setCurrentPath(url);
      } catch (error) {
        console.error(error);
      }
      pushState.call(history, ...args);
    };
    history.replaceState = (...args) => {
      try {
        const url = args[2];
        const { pathname } = window.location;
        owlDebug('current_pathname', pathname);
        owlDebug('url_replace', url);
        /** 如果url 包含 当前pathname, 则认为这是一个层级重定向 ，a -> a/b -> a/b/c，此时，渲染尚未完成，所以不用reset数据 */
        /** 否则，认为是页面的替换，则reset 数据 */
        if (!url.includes(pathname)) {
          /** reset数据时，先上报当前场景剩余未上报的资源 */
          resourceMonitorTimerCallback();
          window.hkOwl.reset(url);
        }
        // 切换场景，无论是层级重定县，还是其他正常重定向，都切换场景
        window.hkOwl._setCurrentPath(url);
      } catch (error) {
        console.error(error);
      }
      replaceState.call(history, ...args);
    };
    window.addEventListener('popstate', (e) => {
      try {
        const url = document.location.pathname;
        owlDebug('url_pop', url);
        /** reset数据时，先上报当前场景剩余未上报的资源 */
        resourceMonitorTimerCallback();
      } catch (error) {
        console.error(error);
      }
      window.hkOwl.reset(url);
    });
  })();
  